﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using LucyGrid.Messaging;
using LucyGrid.Nodes;
using Newtonsoft.Json.Linq;

namespace LucyGrid
{
	/// <summary>
	/// Grid node. Runs in background in the machine listening for grid processing requests.
	/// </summary>
	public class GridNode: IDisposable
	{
		/// <summary>
		/// Endpoints being served by this node
		/// </summary>
		public Dictionary<string, Process> endpoints;
		GridComm bus;
		ManualResetEvent sleep;
		Task tracing;
		string machineName;
		string ip;
		int cores;
		bool overload;
		TimeSpan statusNotificationTimeout;
		string binfile;

		static GridNode()
		{
			GridComm.Initialize();
		}
		/// <summary>
		/// Builds a new node receiving the parameters passed to the processor
		/// </summary>
		/// <param name="args"></param>
		public GridNode(GridUtils.ActorType actorType, string endpoint)
		{
			GridUtils.Mode = actorType;
			binfile = LucyBinManager.BinFile();
			// Default
			switch (GridUtils.Mode) {
				case GridUtils.ActorType.GRIDNODE:
					StartAsNode();
					break;
				case GridUtils.ActorType.WORKER:
					StartAsWorker(endpoint);
					break;
			}
			GridUtils.NotifyLog("NODE,INFO", "GridNode type {0}. Finishing", GridUtils.Mode.ToString());
		}

		/// <summary>
		/// Capture workers output
		/// </summary>
		protected void WorkerTracking() {
			GridPerformance.StartSampling();
			tracing = Task.Run(() =>
			{
				Thread.CurrentThread.Name = string.Format("GRIDNODE WORKERTRACKING-{0}", Thread.CurrentThread.ManagedThreadId);
				do
				{
					ReadWorkerOutputs();
					ClearDeadWorkers();
					CheckLimits();
					PublishStatus(false);
					Thread.Sleep(statusNotificationTimeout);
				} while (!sleep.WaitOne(0));
				GridUtils.NotifyLog("NODE,INFO", "WorkerTracking end"); 
			});
		}

		/// <summary>
		/// Remove finished endpoints to release resources if the don't have error code
		/// Errors will be keep on the list to avoid re-run them.
		/// </summary>
		private void ClearDeadWorkers()
		{
			lock (endpoints)
			{
				// Clear endpoints
				while (endpoints.Any(kv => kv.Value.HasExited && kv.Value.ExitCode == 0))
				{
					var e = endpoints.First(kv => kv.Value.HasExited && kv.Value.ExitCode == 0);
					GridUtils.NotifyLog("NODE,INFO", "Removing finished endpoint: {0}", e.Key);
					endpoints.Remove(e.Key);
				}
			}
		}

		/// <summary>
		/// Checks limits to notify overload status
		/// </summary>
		private void CheckLimits()
		{
			if (GridPerformance.Pause || GridPerformance.IsHighLimitReached()) 
				overload = true;
			else if (GridPerformance.IsLowLimitReached())
				overload = false;
		}
		/// <summary>
		/// Notify grid node status
		/// </summary>
		private void PublishStatus(bool stoping)
		{
			bus.Emit("STATUS", GridCommEvents.status, new NodeStatus() {	
										NodeId = GridComm.Id, 
										Status = stoping ? GridCommStatusEvent.stopping :
													GridPerformance.Pause ? GridCommStatusEvent.paused:
 														overload ? GridCommStatusEvent.overhead:
															endpoints.Count != 0 ? GridCommStatusEvent.running: 
																GridCommStatusEvent.waiting, 
										Memory = GridPerformance.MemoryAvg, 
										Processor = GridPerformance.ProcessorAvg,
										MachineName = machineName,
										Ip = ip,
										Cores = cores,
										Workers = endpoints.Keys
			}); 												
		}

		/// <summary>
		/// Capture workers output
		/// </summary>
		private void ReadWorkerOutputs()
		{
			if (endpoints.Count != 0)
			{
				lock (endpoints)
				{
					// Read outputs
					if (endpoints.Count != 0)
						endpoints
							.Where(kv => kv.Value.HasExited)
							.All(kv =>
							{
								string data = null;
								if((data = kv.Value.StandardError.ReadToEnd()) != null)
									System.Console.WriteLine(string.Format("ERROR: {0}", data));
								if ((data = kv.Value.StandardOutput.ReadToEnd()) != null)
									System.Console.WriteLine(data);
								return true;
							});
				}
			}
		}
		
		/// <summary>
		/// Initialize communications
		/// </summary>
		/// <param name="args"></param>
		private void ConnectToBus()
		{
			bus = new GridComm();
			bus.OnEventReceived += bus_OnEventReceived;
			bus.StartListener();
		}

		/// <summary>
		/// New event received. We wait for GridCommEvents.grid events or maintenance events
		/// </summary>
		/// <param name="eventName"></param>
		/// <param name="ev"></param>
		void bus_OnEventReceived(GridCommEvents eventName, JObject ev)
		{
			 switch (eventName) { 
				case GridCommEvents.grid:
					 if (ev["Chunks"].ToString() != "0")
						 LaunchNewWorker(ev);
					 else
						 RemoveEnpoint(ev);
					break;
				case GridCommEvents.maintenance:
					ProcessMaintenance(ev);
					break;
			}
		}

		/// <summary>
		/// Removes an endpoint from the list.
		/// </summary>
		/// <param name="endpoint"></param>
		private void RemoveEnpoint(JObject ev)
		{
			string endpoint = ev["Endpoint"].ToString();
			lock (endpoints) {
				if (endpoints.ContainsKey(endpoint))
					endpoints.Remove(endpoint);
			}
		}

		/// <summary>
		/// Process a maintance event
		/// </summary>
		/// <param name="ev"></param>
		private void ProcessMaintenance(JObject ev)
		{
			var cmd = ev.Property("Cmd");
			var mname = ev.Property("MachineName");
			if (cmd == null || mname == null)
				return;
			if (!mname.Value.ToString().Equals(GridCommMacros.all.ToString()) 
				&& !mname.Value.ToString().Equals(machineName))
				return;
			if (cmd.Value.ToString().Equals(GridCommMaintenaceEvent.stop.ToString()))
			{
				PublishStatus(true);
				sleep.Set();
				GridUtils.NotifyLog("NODE,INFO", "Stop message recived");
			}
			if (cmd.Value.ToString().Equals(GridCommMaintenaceEvent.pause.ToString()))
			{
				GridPerformance.Pause = true;
				GridUtils.NotifyLog("NODE,INFO", "Pause message recived");
			}
			if (cmd.Value.ToString().Equals(GridCommMaintenaceEvent.resume.ToString()))
			{
				GridPerformance.Pause = false;
				GridUtils.NotifyLog("NODE,INFO", "Resume message recived");
			}
		}

		/// <summary>
		/// Create a worker for each new enpoint received
		/// </summary>
		/// <param name="endpoint"></param>
		private void LaunchNewWorker(JObject ev)
		{
			string endpoint = ev["Endpoint"].ToString();
			lock (endpoints)
			{
				var ep = endpoints.ContainsKey(endpoint) ? endpoints[endpoint] : null;
				// All ready connected
				if (ep != null && !HasExited(ep)) 
					return;
				Process p = null;
				if (ep != null)
				{
					GridUtils.NotifyLog("NODE,INFO", "Exit code: {0}. Last executor info: <{1}: {2}>",
							ep.ExitCode,
							ep.StandardError.ReadToEnd(), 
							ep.StandardOutput.ReadToEnd());
					endpoints.Remove(endpoint);
				}
				var args = string.Format("{0} {1}", GridUtils.ActorType.WORKER, 
						endpoint);
				GridUtils.NotifyLog("NODE,INFO", "Starting WORKER: {0} {1}", binfile, args); 
				p = Process.Start(new ProcessStartInfo()
				{
					FileName = binfile,
					Arguments = args,
					CreateNoWindow = true,
					RedirectStandardError = true,
					RedirectStandardInput = true,
					RedirectStandardOutput = true,
					UseShellExecute = false
				});
				if (p.HasExited)
					GridUtils.NotifyLog("NODE,INFO", "Error creating worker: {0}. {1}", p.StandardError.ReadToEnd(), p.StandardOutput.ReadToEnd());
				if(!p.HasExited || p.ExitCode != 0)
					endpoints.Add(endpoint, p);
			}
			System.Console.WriteLine("GRIDWORKER started for " + endpoint);
		}

		private bool HasExited(Process ep)
		{
			return Process.GetProcesses().All(p => p.Id != ep.Id)
				&& ep.ExitCode == 0;
		}

		/// <summary>
		/// The instance is running as the main resident
		/// </summary>
		/// <param name="args"></param>
		private void StartAsNode()
		{
			machineName = Environment.MachineName;
			// FIX: Error on remote machines
			ip = "0.0.0.0";//Dns.GetHostEntry(machineName).AddressList.First(a => a.IsIPv6LinkLocal).ToString();
			cores = GridPerformance.CoresCount();
			sleep = new ManualResetEvent(false);
			endpoints = new Dictionary<string, Process>();
			statusNotificationTimeout = new TimeSpan(0, 0, 0, 10, 0);
			ConnectToBus();
			WorkerTracking();
			System.Console.WriteLine("GRIDNODE started");
			// Go to sleep
			sleep.WaitOne();
			Finilize();
		}

		/// <summary>
		/// Ensure everything is done
		/// </summary>
		private void Finilize()
		{
			// Reset pause
			GridPerformance.Pause = false;
		}

		/// <summary>
		/// This instance runs as a worker processing chunks
		/// </summary>
		/// <param name="args"></param>
		private void StartAsWorker(string endpoint)
		{
			LucyBinManager.imaster = new LucyMasterRPC(endpoint);
			new GridExecutor(endpoint, LucyBinManager.imaster);
		}

		/// <summary>
		/// Release resources
		/// </summary>
		public void Dispose()
		{
			sleep.Set();
			bus.Dispose();
			tracing.Wait();
		}
	}
}
